all repos — weimar @ 3ab9da929b53350b9e5a860642317e5a8f5b3750

Unnamed repository; edit this file 'description' to name the repository.

web/src/routes/image/[id]/+page.svelte (view raw)

  1<script lang="ts">
  2	import { onMount } from 'svelte';
  3	import { getImage, imageDownloadUrl, thumbnailUrl, type ImageDetail } from '$lib/api';
  4
  5	let { params } = $props();
  6	let id = $derived(parseInt(params.id));
  7
  8	let img = $state<ImageDetail | null>(null);
  9	let loading = $state(true);
 10	let error = $state('');
 11
 12	onMount(async () => {
 13		try {
 14			img = await getImage(id);
 15		} catch (err) {
 16			error = err instanceof Error ? err.message : 'Failed to load image';
 17		} finally {
 18			loading = false;
 19		}
 20	});
 21
 22	function formatSize(bytes: number): string {
 23		if (bytes < 1024) return bytes + ' B';
 24		if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
 25		return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
 26	}
 27
 28	function formatDate(s: string): string {
 29		return new Date(s).toLocaleString();
 30	}
 31</script>
 32
 33{#if loading}
 34	<p>Loading...</p>
 35{:else if error}
 36	<p class="error">{error}</p>
 37{:else if img}
 38	<h1>{img.filename}</h1>
 39
 40	<div class="detail">
 41		<div class="preview">
 42			{#if img.thumbnail_path}
 43				<a href={imageDownloadUrl(img.id)} download={img.filename}>
 44					<img src={thumbnailUrl(img.id)} alt={img.filename} />
 45				</a>
 46			{:else}
 47				<div class="placeholder">No thumbnail</div>
 48			{/if}
 49		</div>
 50
 51		<div class="meta">
 52			<dl>
 53				<dt>Dimensions</dt>
 54				<dd>{img.width} &times; {img.height}</dd>
 55				<dt>File size</dt>
 56				<dd>{formatSize(img.file_size)}</dd>
 57				<dt>Type</dt>
 58				<dd>{img.mime_type}</dd>
 59				<dt>Tags</dt>
 60				<dd>
 61					{#if img.tags_detail.length > 0}
 62						<div class="tag-pills">
 63							{#each img.tags_detail as t (t.tag)}
 64								<a href="/browse?tag={encodeURIComponent(t.tag)}" class="tag-pill" title="Used {t.usage_count} time{t.usage_count === 1 ? '' : 's'}">
 65									<span class="tag-name">{t.tag}</span>
 66									<span class="tag-count">{t.usage_count}</span>
 67								</a>
 68							{/each}
 69						</div>
 70					{:else}
 71						<span class="dim">none</span>
 72					{/if}
 73				</dd>
 74				<dt>Uploaded</dt>
 75				<dd>{formatDate(img.created_at)}</dd>
 76			</dl>
 77
 78			<a href={imageDownloadUrl(img.id)} class="download" download={img.filename}>
 79				Download original
 80			</a>
 81		</div>
 82	</div>
 83
 84	<p class="back"><a href="/browse">&larr; Back to browse</a></p>
 85{/if}
 86
 87<style>
 88	.detail {
 89		display: flex;
 90		gap: 2rem;
 91		margin-top: 1rem;
 92		flex-wrap: wrap;
 93	}
 94	.preview img {
 95		max-width: 100%;
 96		max-height: 60vh;
 97		border-radius: 8px;
 98		box-shadow: 0 2px 8px rgba(0,0,0,0.15);
 99	}
100	.placeholder {
101		width: 300px;
102		height: 200px;
103		display: flex;
104		align-items: center;
105		justify-content: center;
106		background: #f0f0f0;
107		border-radius: 8px;
108		color: #999;
109	}
110	.meta {
111		min-width: 220px;
112	}
113	dl {
114		margin: 0;
115	}
116	dt {
117		font-weight: 600;
118		margin-top: 0.5rem;
119		font-size: 0.85rem;
120		color: #555;
121	}
122	dd {
123		margin: 0;
124		font-size: 0.95rem;
125	}
126	.dim {
127		color: #999;
128	}
129	.download {
130		display: inline-block;
131		margin-top: 1.5rem;
132		padding: 0.5rem 1.5rem;
133		background: #1a1a2e;
134		color: #eee;
135		text-decoration: none;
136		border-radius: 6px;
137	}
138	.download:hover {
139		background: #16213e;
140	}
141	.back {
142		margin-top: 2rem;
143	}
144
145	.tag-pills {
146		display: flex;
147		flex-wrap: wrap;
148		gap: 0.4rem;
149		margin-top: 0.25rem;
150	}
151	.tag-pill {
152		display: inline-flex;
153		align-items: center;
154		gap: 0.35rem;
155		background: #1a1a2e;
156		border-radius: 20px;
157		padding: 0.3rem 0.65rem 0.3rem 0.8rem;
158		font-size: 0.85rem;
159		color: #eee;
160		text-decoration: none;
161		white-space: nowrap;
162		transition: background 0.15s;
163	}
164	.tag-pill:hover {
165		background: #16213e;
166	}
167	.tag-name {
168		font-weight: 500;
169	}
170	.tag-count {
171		background: rgba(255, 255, 255, 0.15);
172		border-radius: 10px;
173		padding: 0.06rem 0.4rem;
174		font-size: 0.72rem;
175		font-weight: 600;
176		color: rgba(255, 255, 255, 0.75);
177		min-width: 1.2rem;
178		text-align: center;
179	}
180
181	.error {
182		color: #c0392b;
183	}
184</style>